/*
 * MIT License
 *
 * Copyright (c) 2024-2048 冰羽
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.star.framework.jwt.controller;

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.star.framework.autoconfigure.jwt.TokenProperties;
import cn.star.framework.autoconfigure.security.SecurityProperties;
import cn.star.framework.core.api.AppResult;
import cn.star.framework.jwt.core.JwtTokenProvider;
import cn.star.framework.jwt.core.SecurityPasswordEncoder.PasswordValidateException;
import cn.star.framework.jwt.core.SecurityUser;
import cn.star.framework.jwt.dto.CurrentUser;
import cn.star.framework.jwt.security.entity.Token;
import cn.star.framework.jwt.security.service.TokenService;
import cn.star.framework.jwt.types.TokenType;
import cn.star.framework.rbac.entity.Resource;
import cn.star.framework.rbac.entity.Role;
import cn.star.framework.rbac.entity.User;
import cn.star.framework.rbac.service.AuthorizationService;
import cn.star.framework.rbac.service.UserRoleService;
import cn.star.framework.rbac.service.UserService;
import cn.star.framework.rbac.type.ResourceType;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用户认证<br>
 *
 * @author zhaoweiping
 *     <p style='color: red'>Created on 2024-09-23 11:06:00
 * @since 3.0.0
 */
@Slf4j
@RestController
@Api(tags = "认证管理")
public class AuthenticationController {
  @Autowired private JwtTokenProvider provider;
  @Autowired private AuthenticationManager authenticationManager;
  @Autowired private UserService userService;
  @Autowired private AuthorizationService authorizationService;
  @Autowired private UserRoleService userRoleService;
  @Autowired private TokenService tokenService;
  @Autowired private TokenProperties token;
  @Autowired private SecurityProperties security;

  static class UsernamePasswordAuthenticationTokenImpl extends UsernamePasswordAuthenticationToken
      implements Serializable {
    @Getter @Setter private String username;
    @Getter @Setter private String password;

    public UsernamePasswordAuthenticationTokenImpl(String principal, String credentials) {
      super(principal, credentials);
      this.username = principal;
      this.password = credentials;
    }

    public UsernamePasswordAuthenticationTokenImpl(
        String principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
      super(principal, credentials, authorities);
      this.username = principal;
      this.password = credentials;
    }
  }

  private UsernamePasswordAuthenticationTokenImpl build(
      HttpServletRequest request, User user, String username, String password) {
    if (user != null && StrUtil.isNotBlank(user.getUsername())) {
      return new UsernamePasswordAuthenticationTokenImpl(user.getUsername(), user.getPassword());
    } else if (StrUtil.isNotBlank(username)) {
      return new UsernamePasswordAuthenticationTokenImpl(username, password);
    } else {
      return null;
    }
  }

  @ApiOperation(value = "登录认证")
  @PostMapping(value = "/authenticate")
  public AppResult<String> authenticate(
      HttpServletRequest request,
      @RequestBody(required = false) User user,
      @RequestParam(value = "username", required = false) String username,
      @RequestParam(value = "password", required = false) String password) {
    UsernamePasswordAuthenticationTokenImpl at = this.build(request, user, username, password);
    if (at == null) {
      return AppResult.ofFail("未获取到登录参数，请检查！");
    } else {
      try {
        Authentication authentication = authenticationManager.authenticate(at);

        SecurityUser su = (SecurityUser) authentication.getPrincipal();

        Map<String, Object> claims = new HashMap<>(1);
        claims.put("authorities", su.getAuthorities());

        String accessToken = provider.token(su.getUsername(), "Login", claims);

        // 额外的信息
        Map<String, Object> map = new HashMap<>(3);
        // Token名称，前端下次请求时需要将登录获取的Token再次提交，这个名称添加Token到请求头、参数或者Cookie中
        map.put("access", token.getName());
        // Token刷新名称，前端从响应头中获取刷新Token是使用的名称，登录后需要存储
        map.put("refresh", token.getRefreshName());
        // 描述信息
        map.put(
            "remark",
            "请将获取到的Token放置在之后的请求中，支持请求头、请求参数、Cookie；"
                + "默认支持自动刷新（在一定时间内持续系统的访问自动延时Token有效时间）；"
                + "支持手动刷新（需要调用`/refresh_token`获取新的Token替换现有Token）");

        if (token.isPersistence()) {
          Token token =
              tokenService.save(
                  new Token(accessToken, ServletUtil.getClientIP(request), TokenType.ACCESS_TOKEN));
          // 存储Token后，将Id返回，可以在Token管理中标记当前用户的Token
          map.put("id", token.getId());
        }

        // 设置用户状态
        userService.unlock(su.getRbacUser());

        AppResult<String> result = AppResult.ofSuccess(accessToken);
        result.setAdditional(map);

        return result;
      } catch (Exception e) {
        String message = e.getMessage();
        if (e instanceof PasswordValidateException) {
          String errorMessage = this.locked(at.getUsername());
          message = errorMessage == null ? message : errorMessage;
        }
        return AppResult.ofFail(message);
      }
    }
  }

  private String locked(String username) {
    try {
      if (security.isEnabled()) {
        int times = security.getPasswordErrorTimes();
        User user = userService.findByUsername(username);

        user.setPasswordErrorTimes(user.getPasswordErrorTimes() + 1);
        user.setAccountNonLocked(user.getPasswordErrorTimes() < times);

        userService.save(user);

        String message =
            "您的登录密码输入错误" + user.getPasswordErrorTimes() + "次，连续累计输错" + times + "次（含）将被锁定无法登录！";
        if (!user.isAccountNonLocked()) {
          // 用户已锁定
          message = "您的登录密码已经连续输入错误" + user.getPasswordErrorTimes() + "次（含），账户已被锁定！";
        }

        return message;
      }
    } catch (Exception e) {
      // 忽略异常
      log.warn(e.getMessage());
    }

    return null;
  }

  @ApiOperation(value = "Token刷新(手动)")
  @PostMapping(value = "/refresh_token")
  public AppResult<String> refreshToken(HttpServletRequest request) {
    try {
      String accessToken = provider.getAccessToken(request);
      String tokenValue = provider.refresh(accessToken);

      if (token.isPersistence()) {
        Token refreshToken =
            new Token(tokenValue, ServletUtil.getClientIP(request), TokenType.REFRESH_TOKEN);
        refreshToken.setPrimitiveToken(accessToken);
        tokenService.save(refreshToken);
      }

      return AppResult.ofSuccess(tokenValue);
    } catch (Exception e) {
      log.error("Token refresh fail, ", e);
      return AppResult.ofFail(e.getMessage());
    }
  }

  @ApiOperation(value = "系统注销")
  @RequestMapping(value = "/logout")
  public AppResult<String> logout(HttpServletRequest request) {
    try {
      String accessToken = provider.getAccessToken(request);

      if (token.isPersistence()) {
        tokenService.deleteByAccessToken(accessToken);
      }

      return AppResult.ofSuccess();
    } catch (Exception e) {
      log.error("cancel fail, ", e);
      return AppResult.ofFail(e.getMessage());
    }
  }

  @ApiOperation(value = "当前用户")
  @GetMapping(value = "/getCurrentUser")
  @SuppressWarnings("rawtypes")
  public AppResult<CurrentUser> getCurrentUser(@AuthenticationPrincipal String username) {
    User user = userService.findByUsername(username);

    List<Role> roles = userRoleService.findRolesByUserId(user.getId());

    Map<ResourceType, List<Resource>> resources =
        authorizationService.getUserAuthorizationResources(user.getId());

    CurrentUser cu = new CurrentUser(username, user.getName(), roles, resources);

    return AppResult.ofSuccess(cu);
  }
}
